/*******************************************************************************
 * Copyright 2011-2012 Dik Grapendaal
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package sh.grapendaal.tsuushin.service.impl;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.apache.commons.io.monitor.FileAlterationObserver;
import org.apache.felix.framework.cache.BundleCache;
import org.apache.felix.main.AutoProcessor;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleException;
import org.osgi.framework.BundleListener;
import org.osgi.framework.Constants;
import org.osgi.framework.launch.Framework;
import org.osgi.framework.launch.FrameworkFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import sh.grapendaal.tsuushin.message.IncomingMessage;
import sh.grapendaal.tsuushin.module.BundleProcessor;
import sh.grapendaal.tsuushin.module.ModuleContainer;
import sh.grapendaal.tsuushin.service.ConfigService;
import sh.grapendaal.tsuushin.service.ModuleService;
import sh.grapendaal.tsuushin.tracking.Context;

@Service
public class ModuleServiceImpl implements ModuleService, BundleListener {
	private static final Logger logger = LoggerFactory.getLogger(ModuleServiceImpl.class);

	private static final String[] OSGI_DEPLOY_ACTIONS = new String[] { AutoProcessor.AUTO_DEPLOY_INSTALL_VALUE,
			AutoProcessor.AUTO_DEPLOY_START_VALUE, AutoProcessor.AUTO_DEPLOY_UPDATE_VALUE,
			AutoProcessor.AUTO_DEPLOY_UNINSTALL_VALUE };

	private static final String BASE_PACKAGE = sh.grapendaal.tsuushin.Main.class.getPackage().getName();
	private static final String[] OSGI_EXTRA_SYSTEMPACKAGES = new String[] { "core", "annotation", "message",
			"message.outgoing", "message.incoming", "service", "tracking" };

	private static final String OSGI_CACHE_DIRECTORY = "cache";
	private static final String MODULE_DIRECTORY = "modules";
	private static final String MODULE_PACKAGE_SUFFIX = ".jar";

	@Autowired
	private BundleProcessor bundleProcessor;

	@Autowired
	private ConfigService configService;

	private Framework osgiFramework;
	private FileAlterationMonitor fileMonitor;
	private Map<String, String> osgiConfiguration = new HashMap<String, String>();
	private Map<Long, ModuleContainer> modules = new HashMap<Long, ModuleContainer>();

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void bundleChanged(BundleEvent event) {
		Bundle bundle = event.getBundle();

		switch (event.getType()) {
		case BundleEvent.STARTED: {
			logger.debug("Bundle installed: {}", bundle.getLocation());
			ModuleContainer module = bundleProcessor.processBundle(bundle);
			if (module != null) {
				try {
					module.instantiate();
					modules.put(bundle.getBundleId(), module);
				} catch (Exception e) {
					logger.error("Could not instantiate module {}.", module.getMainClass(), e);
				}
			} else {
				logger.error("Bundle could not be processed: {}", bundle.getLocation());
			}

			break;
		}

		case BundleEvent.STOPPED: {
			logger.debug("Module stopped: {}", bundle.getLocation());
			ModuleContainer module = modules.remove(bundle.getBundleId());
			if (module != null) {
				module.destroy();
			}

			break;
		}
		}
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public void dispatchCallbacks(Context context, IncomingMessage message) {
		if (modules == null) {
			return;
		}

		for (ModuleContainer container : modules.values()) {
			container.dispatchCallbacks(context, message);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void start() throws BundleException, Exception {
		init();

		osgiFramework.start();
		osgiFramework.getBundleContext().addBundleListener(this);

		fileMonitor.start();

		refreshModules();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void stop() {
		try {
			if (fileMonitor != null) {
				fileMonitor.stop(1000);
			}
		} catch (Exception e) {
			logger.error("Error while stopping FileAlterationMonitor.", e);
		}

		try {
			if (osgiFramework != null) {
				osgiFramework.getBundleContext().removeBundleListener(this);
				osgiFramework.stop();
				osgiFramework.waitForStop(0);
			}
		} catch (BundleException e) {
			logger.error("Error while stopping OSGi framework.", e);
		} catch (InterruptedException e) {
			logger.error("OSGi framework waitForStop() interrupted.", e);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void refreshModules() {
		logger.debug("Refreshing modules...");
		AutoProcessor.process(osgiConfiguration, osgiFramework.getBundleContext());
	}

	/**
	 * Initializes the OSGi framework of our choice. In this case, Apache Felix.
	 * 
	 * @throws Exception
	 *             If the framework could not be initialized.
	 */
	private void init() throws Exception {
		String moduleDir = MODULE_DIRECTORY;
		if (!configService.getRelativeWorkingDirectory().isEmpty()) {
			String workingDir = configService.getRelativeWorkingDirectory();
			osgiConfiguration.put(BundleCache.CACHE_ROOTDIR_PROP, workingDir);

			moduleDir = workingDir + "/" + moduleDir;
		}

		logger.debug("Initializing Module Service with module directory {}.", moduleDir);

		this.initConfiguration(moduleDir);

		osgiFramework = getFrameworkFactory().newFramework(osgiConfiguration);
		osgiFramework.init();

		File moduleDirFile = new File(moduleDir);
		if (!moduleDirFile.exists()) {
			moduleDirFile.mkdir();
		}

		FileAlterationObserver observer = new FileAlterationObserver(moduleDirFile, FileFilterUtils.and(
				FileFilterUtils.fileFileFilter(), FileFilterUtils.suffixFileFilter(MODULE_PACKAGE_SUFFIX)));
		observer.addListener(new FileAlterationListenerAdaptor() {
			@Override
			public void onFileChange(File file) {
				refreshModules();
			}

			@Override
			public void onFileCreate(File file) {
				refreshModules();
			}

			@Override
			public void onFileDelete(File file) {
				refreshModules();
			}
		});

		fileMonitor = new FileAlterationMonitor();
		fileMonitor.addObserver(observer);
	}

	/**
	 * Sets the configuration options needed for the OSGi framework.
	 * 
	 * @param moduleDir
	 *            The directory where the modules can be found.
	 */
	private void initConfiguration(String moduleDir) {
		osgiConfiguration.put(AutoProcessor.AUTO_DEPLOY_DIR_PROPERY, moduleDir);
		osgiConfiguration.put(Constants.BUNDLE_CLASSPATH, ".");
		osgiConfiguration.put(Constants.FRAMEWORK_STORAGE, OSGI_CACHE_DIRECTORY);
		osgiConfiguration.put(Constants.FRAMEWORK_STORAGE_CLEAN, "true");

		StringBuilder builder = new StringBuilder();
		for (String action : OSGI_DEPLOY_ACTIONS) {
			builder.append(",").append(action);
		}
		osgiConfiguration.put(AutoProcessor.AUTO_DEPLOY_ACTION_PROPERY, builder.substring(1));
		
		logger.debug("OSGi configuration: {} = {}", AutoProcessor.AUTO_DEPLOY_ACTION_PROPERY, builder.substring(1));

		builder = new StringBuilder();
		for (String subPackage : OSGI_EXTRA_SYSTEMPACKAGES) {
			builder.append(",").append(BASE_PACKAGE).append(".").append(subPackage);
		}
		osgiConfiguration.put(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, builder.substring(1));
		
		logger.debug("OSGi configuration: {} = {}", Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, builder.substring(1));
	}

	/**
	 * Gets the FrameworkFactory for the OSGi framework implementation we're
	 * using here.
	 * 
	 * @return A FrameworkFactory we can use to instantiate the actual
	 *         framework.
	 * @throws Exception
	 *             If no FrameworkFactory could be found, accessed or
	 *             instantiated.
	 */
	private FrameworkFactory getFrameworkFactory() throws Exception {
		URL url = org.apache.felix.main.Main.class.getClassLoader().getResource(
				"META-INF/services/org.osgi.framework.launch.FrameworkFactory");

		if (url == null) {
			throw new Exception("No FrameworkFactory class found.");
		}

		BufferedReader reader = null;
		try {
			reader = new BufferedReader(new InputStreamReader(url.openStream()));
			for (String line = reader.readLine(); line != null; line = reader.readLine()) {
				line = line.trim();
				// Try to load first non-empty, non-commented line.
				if (!line.isEmpty() && line.charAt(0) != '#') {
					return (FrameworkFactory) Class.forName(line).newInstance();
				}
			}
		} catch (IOException e) {
			throw new Exception("IO Exception while processing file.", e);
		} catch (ClassNotFoundException e) {
			throw new Exception("FrameworkFactory class could not be found.", e);
		} catch (Exception e) {
			throw new Exception("FrameworkFactory class could not be instantiated.", e);
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) {
					// Ignore.
				}
			}
		}

		throw new Exception("No FrameworkFactory class found.");
	}
}
